微前端

  1. 哪里使用了
  2. 为什么要使用
  3. 原理是怎么样的
  4. 做了哪些优化
  5. 得到的结果怎么样

为什么需要微前端

在 toB 的前端开发工作中,我们往往就会遇到如下困境:

  1. 工程越来越大,打包越来越慢
  2. 团队人员多,产品功能复杂,代码冲突频繁、影响面大
  3. 内心想做 SaaS 产品,但客户总是要做定制化

微前端的实现,意味着对前端应用的拆分。拆分应用的目的,并不只是为了架构上好看,还为了提升开发效率。

微前端带来这么一系列的好处:

  1. 应用自治。只需要遵循统一的接口规范或者框架,以便于系统集成到一起,相互之间是不存在依赖关系的。
  2. 单一职责。每个前端应用可以只关注于自己所需要完成的功能。
  3. 技术栈无关。你可以使用 Angular 的同时,又可以使用 React 和 Vue。

除此,它也有一系列的缺点:

  1. 应用的拆分基础依赖于基础设施的构建,一旦大量应用依赖于同一基础设施,那么维护变成了一个挑战。
  2. 拆分的粒度越小,便意味着架构变得复杂、维护成本变高。
  3. 技术栈一旦多样化,便意味着技术栈混乱。

微前端的基本要求

  1. 应用可分离,单独维护和发布
  2. 状态隔离,但部分状态又可共享

如何设计微前端架构

就当前而言,要设计出一个微前端应用不是一件容易的事——还没有最佳实践。在不同的落地案例里,使用的都是不同的方案。出现这种情况的主要原因是,每个项目所面临的情况、所使用的技术都不尽相同。为此,我们需要了解一些基础的微前端模式。

1. 架构模式

微前端应用间的关系来看,分为两种:基座模式(管理式)、自组织式。分别也对应了两者不同的架构模式:

基座模式。通过一个主应用,来管理其它应用。设计难度小,方便实践,但是通用度低。 自组织模式。应用之间是平等的,不存在相互管理的模式。设计难度大,不方便实施,但是通用度高。

就当前而言,基座模式实施起来比较方便,方案上便也是蛮多的。 img

而不论种方式,都需要提供一个查找应用的机制,在微前端中称为服务的注册表模式。和微服务架构相似,不论是哪种微前端方式,也都需要有一个应用注册表的服务,它可以是一个固定值的配置文件,如 JSON 文件,又或者是一个可动态更新的配置,又或者是一种动态的服务。它主要做这么一些内容:

应用发现。让主应用可以寻找到其它应用。应用注册。即提供新的微前端应用,向应用注册表注册的功能。第三方应用注册。即让第三方应用,可以接入到系统中。访问权限等相关配置。 img 应用在部署的时候,便可以在注册表服务中注册。如果是基于注册表来管理应用,那么使用基座模式来开发比较方便。

2. 设计理念

在笔者实践微前端的过程中,发现了以下几点是我们在设计的过程中,需要关注的内容:

中心化:应用注册表。这个应用注册表拥有每个应用及对应的入口。在前端领域里,入口的直接表现形式可以是路由,又或者对应的应用映射。 标识化应用: 我们需要一个标识符来标识不同的应用,以便于在安装、卸载的时候,能寻找到指定的应用。一个简单的模式,就是通过康威定律来命名应用。应用生命周期管理。高内聚,低耦合。

3. 生命周期

前端微架构与后端微架构的最大不同之处,也在于此——生命周期。微前端应用作为一个客户端应用,每个应用都拥有自己的生命周期:

Load,决定加载哪个应用,并绑定生命周期 bootstrap,获取静态资源 Mount,安装应用,如创建 DOM 节点 Unload,删除应用的生命周期 Unmount,卸载应用,如删除 DOM 节点、取消事件绑定.

这部分的内容事实上,也就是微前端的一个难点所在,如何以合适的方式来加载应用——毕竟每个前端框架都各自不同,其所需要的加载方式也是不同的。当我们决定支持多个框架的时候,便需要在这一部分进入更细致的研究。 随后,我们要面临的一个挑战是:如何去拆分应用。

4. 技术方式

从技术实践上,微前端架构可以采用以下的几种方式进行:

  1. 路由分发式。通过 HTTP 服务器的反向代理功能,来将请求路由到对应的应用上。
  2. 前端微服务化。在不同的框架之上设计通讯、加载机制,以在一个页面内加载对应的应用。
  3. 微应用。通过软件工程的方式,在部署构建环境中,组合多个独立应用成一个单体应用。
  4. 微件化。开发一个新的构建系统,将部分业务功能构建成一个独立的 chunk 代码,使用时只需要远程加载即可。
  5. 前端容器化。通过将 iFrame 作为容器,来容纳其它前端应用。
  6. 应用组件化。借助于 Web Components 技术,来构建跨框架的前端应用。

实施的方式虽然多,但是都是依据场景而采用的。有些场景下,可能没有合适的方式;有些场景下,则可以同时使用多种方案。

微前端怎么做?

我们主要针对狭义微前端的目标,也就是对大型工程进行解耦,这里有两个基本问题:

  1. 子应用之间是否需要隔离,是否支持技术栈不统一?这个问题很关键,目前主流的方案都是选择隔离的,如果选择不隔离,那一般是技术栈统一的,在组件和工具维度做集成。比如美团外卖的方案open in new window ,这种方案主子工程之间耦合程度高(需要路由加载约定,组件动态加载约定,store 加载约定,共享组件和工具等等),维护成本高,从某种程度上来说是违背我们想要降低巨石工程维护困难的初衷的。但这也同样是它的优势,子工程之间的集成和复用程度可以更高。
  2. 子应用加载时机?路由切换或者手动加载?目前大部分的场景是通过切换菜单更改路由来切换子应用,手动加载的情况较少,不过也是有相应场景的,新版的 Qiankun 基于 Single-spa 的 Parcelopen in new window 做了这方面的支持。

接下来我们看一下典型微前端方案需要回答的三个最关键问题:

  1. 子应用如何定义和使用?
  2. 如何动态加载?
  3. 如何隔离?

Single-spa

几年前面世的 single-spa 解决了第一个问题,我们来看一下它的用法,截图自Single-spa 官网open in new window

img

img

我们可以看到,关键 api 是 registerApplication,这里需要传入 app,它一般是个函数,返回一个 promise,resolve 值需要包含 mount, unmount 和 bootstrap 等子应用生命周期方法,这个设计是比较有想象空间的,只要符合这个规则,我们可以自己去实现这个函数,加入自己的规则,Qiankun 正是这样做的。

另一个参数 activeWhen 就是根据路由加载子应用的规则,single-spa 会监听路由变化并劫持更改原生方法,应用路由判断逻辑。部分代码如下:

window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
window.history.pushState = patchedUpdateState(
  window.history.pushState,
  "pushState"
);

子应用加载之后,single-spa 会维护子应用的实例,再次激活时会直接使用。

Single-spa 主要解决了三个问题中的第一个,对于第二个和第三个没有给出具体方案,Qiankun 填补了这个空白。

Qiankun

首先看一下它的用法:

img

关键 api registerMicroApps 与 single-spa 类似,都是注册子应用,区别在于支持传入 html 地址或者前端资源地址的数组作为应用入口,根据约定,子应用需要暴露声明周期方法,Qiankun 会去加载资源然后根据约定拿到方法,这里官方的推荐是通过 webpack 的 umd 输出格式来做。在执行 js 资源时通过 eval,会将 window 绑定到一个 Proxy 对象上,以防污染全局变量,并方便对脚本的 window 相关操作做劫持处理,达到子应用之间的脚本隔离。 下面是截取的一些相关代码,逻辑还算比较容易看懂。

img

其中子应用代码真正的执行过程在拆分出来的 import-html-entry 模块中:

img

而样式隔离则是通过在 unmount 时卸载样式表自然地做到。

就这样,Qiankun 解决了剩余的两个问题——加载和隔离,成为了一个完整的微前端方案。

当然,除了这三个主要问题,还有一些次要的问题需要解决,比如:如何共享基础库?子应用如何与主应用联调?等等。 随着实践微前端概念的团队越来越多,Qiankun 也在不断的完善,这里有新版功能和后续计划的介绍open in new window

最后我想说,微前端并不是银弹,它只是为了解决特定问题而产生的方案,而且有自己的弊端,比如下图中阿里云的总结。因此如果没有文中提到的痛点,大可不必费力尝试。

如何实现插件化 & 按需加载

插件化就考虑 Core+Plugin 的设计,按需加载就考虑将插件单独打成 npm 包

santa 方案

  1. webpack 构建入口文件
    1. js
    2. css
  2. 主应用进入之后,通过 plugins 接口获取所有插件的列表
    1. 紧接着加载扩展的入口文件(onebox 入口,只标注应用的 js/css 文件),注册应用的静态资源信息到主应用中。
  3. loader 主动加载 js 入口文件

缺点:

  1. 不同应用不能单独发布,只能永远加载线上最新代码。

qiankun 方案

优势:

  1. 样式隔离
  2. 可以给应用单独发布

qiankun 的隔离处理

兼容 IE11 的沙箱能力

在 qiankun issue 区域呼声最高的就是 IE 的兼容open in new window,有不少小伙伴都期待 qiankun 能够在 IE 下使用。

qiankun 1.x 在 IE 使用的主要阻碍就是 qiankun 的沙箱使用了 ES6 的 Proxy,而这无法通过 ployfill 等方式弥补。这导致 IE 下的 qiankun 用户无法开启 qiankun 的沙箱功能,导致 js 隔离、样式隔离这些能力都无法启用。

为此,我们实现了一个 IE 特供的快照沙箱,用于这些不支持 Proxy 的浏览器;这不需要用户手动开启,在代理沙箱不支持的环境中,我们会自动降级到快照沙箱。

注意,由于快照沙箱不能做到互相之间的完全独立,所以 IE 等环境下我们不支持多应用场景, singlur 会被强制设为 true。

基于 shadow DOM 的样式隔离

样式隔离也是微前端面临的一个重要问题,在 qiankun@1.x 中,我们支持了微应用之间的样式隔离(仅沙箱开启时生效),这尚存一些问题:

  1. 主子应用之间的样式隔离依赖手动配置插件处理
  2. 多应用场景下微应用之间的样式隔离亟待处理

为此,我们引入了一个新的选项, sandbox: { strictStyleIsolation?: boolean }

在该选项开启的情况下,我们会以 Shadow DOM 的形式嵌入微应用,以此来做到应用样式的真正隔离:

import { loadMicroApp } from "qiankun";

loadMicroApp({ xxx }, { sandbox: { strictStyleIsolation: true } });

Shadow DOM 可以做到样式之间的真正隔离(而不是依赖分配前缀等约定式隔离),其形式如下:

image.png

在开启 strictStyleIsolation 时,我们会将微应用插入到 qiankun 创建好的 Shadow Tree 中,微应用的样式(包括动态插入的样式)都会被挂载到这个 Shadow Host 节点下,因此微应用的样式只会作用在 Shadow Tree 内部,这样就做到了样式隔离。

但是开启 Shadow DOM 也会引发一些别的问题:

一个典型的问题是,一些组件可能会越过 Shadow Boundary 到外部 Document Tree 插入节点,而这部分节点的样式就会丢失;比如 antd 的 Modal 就会渲染节点至 document.body ,引发样式丢失;针对刚才的 antd 场景你可以通过他们提供的 ConfigProvider.getPopupContainer API 来指定在 Shadow Tree 内部的节点为挂载节点,但另外一些其他的组件库,或者你的一些代码也会遇到同样的问题,需要你额外留心。

此外 Shadow DOM 场景下还会有一些额外的事件处理、边界处理等问题,后续我们会逐步更新官方文档指导用户更顺利的开启 Shadow DOM。

所以请根据实际情况来选择是否开启基于 shadow DOM 的样式隔离,并做好相应的检查和处理。

qiankun 的通信

微前端场景下,我们认为最合理的通信方案是通过 URL 及 CustomEvent 来处理。但在一些简单场景下,基于 props 的方案会更直接便捷,因此我们为 qiankun 用户提供这样一组 API 来完成应用间的通信:

主应用创建共享状态:

import { initGlobalState } from "qiankun";

initGlobalState({ user: "kuitos" });

微应用通过 props 获取共享状态并监听:

export function mount(props) {
  props.onGlobalStateChange((state, prevState) => {
    console.log(state, prevState);
  });
}

qiankun 解决了 single-spa 什么问题

  1. 共享依赖解决方案:在微前端架构中,不同的子应用可能会使用相同的依赖库,而传统的单体应用打包会将所有依赖库都打包在一起,导致重复加载和资源浪费。qiankun 提供了共享依赖解决方案,能够将公共的依赖库提取出来,在主应用和子应用之间共享使用,减少重复加载和资源冗余。

  2. 资源隔离和沙箱环境:在微前端架构中,每个子应用都应该运行在独立的沙箱环境中,以确保子应用之间的代码和样式互相隔离,避免相互影响。qiankun 提供了对子应用的隔离和沙箱化支持,每个子应用都运行在独立的 iframe 中,确保各个子应用之间彼此独立,并提供了沙箱机制,防止子应用之间的全局变量污染。

  3. 状态管理和通信机制:在微前端架构中,不同的子应用之间可能需要进行状态共享和通信。qiankun 提供了一套完善的状态管理和通信机制,可以实现子应用之间的状态共享、事件发布订阅等功能,使得子应用之间可以进行数据的交互和通信。

  4. 动态加载和按需加载:在微前端架构中,子应用的加载应该是动态的,只有在需要时才进行加载,以提升整体性能和用户体验。qiankun 支持子应用的动态加载和按需加载,可以根据需求来动态加载子应用,并且能够实现子应用的懒加载,提高启动速度和页面响应性。

总之,qiankun 在 single-spa 的基础上进行了改进和优化,解决了微前端架构中的共享依赖、资源隔离、状态管理和通信机制、动态加载等问题,提供了更好的开发体验和性能优化。

qiankun 源码实现

  1. 注册微应用时通过 fetch 请求 HTML entry,然后正则匹配得到内部样式表、外部样式表、内部脚本、外部脚本
  2. 通过 fetch 获取外部样式表、外部脚本然后与内部样式表、内部脚本按照原来的顺序组合组合之前为样式添加属性选择器(data-微应用名称);将组合好的样式通过 style 标签添加到 head 中。
  3. 创建 js 沙盒:不支持 Proxy 的用 SnapshotSandbox(通过遍历 window 对象进行 diff 操作来激活和还原全局环境),支持 Proxy 且只需要单例的用 LegcySandbox(通过代理来明确哪些对象被修改和新增以便于卸载时还原环境),支持 Proxy 且需要同时存在多个微应用的用 ProxySandbox(创建了一个 window 的拷贝对象,对这个拷贝对象进行代理,所有的修改都不会在 rawWindow 上进行而是在这个拷贝对象上),最后将这个 proxy 对象挂到 window 上面
  4. 执行脚本:将上下文环境绑定到 proxy 对象上,然后 eval 执行

微前端框架

  1. qcmagic
  2. 自研微前端
  3. qiankun
  4. micro App: Web Component

Single-spa

Parcel

Last Updated:
Contributors: yiliang114